/*
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package au.com.domlogic.pvc.core.element;

import au.com.domlogic.pvc.core.attr.AttrMold;
import au.com.domlogic.pvc.core.attr.AttrMold.ElementDecorator;
import au.com.domlogic.pvc.core.component.PVCComponent;
import au.com.domlogic.pvc.core.component.PVCComponentDecorator;
import au.com.domlogic.pvc.core.component.ComponentMold;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * This mold allows the developer to shape an {@link Element} outside of the
 * Document Context.
 *
 * Once the mold is instantiated and configured, developers can create
 * {@link Casting}s from it by passing a {@link Document}.
 * Developers may also create {@link ComponentDecorator}s or {@link ElementDecorator}s
 * by passing {@link PVCComponent}s or {@link PVCElement}s repectively, to the
 * respective constructors.
 *
 * @author Domenic Figliomeni
 */
public class ElementMold extends ComponentMold implements Serializable {

    private String nameSpace;
    private String tagName;
    private String content;
    private Set<AttrMold> attrMolds;

    protected ElementMold() {
        super();
        this.attrMolds = new HashSet<AttrMold>();
    }

    public ElementMold(String nameSpace, String tagName) {
        this();
        this.nameSpace = nameSpace;
        this.tagName = tagName;
    }

    /**
     * @return the nameSpace
     */
    public String getNameSpace() {
        return nameSpace;
    }

    /**
     * @return the tagName
     */
    public String getTagName() {
        return tagName;
    }

    /**
     * @return the {@link Set} of attribute models held by this element model
     */
    public Set<AttrMold> getAttrMolds() {
        return attrMolds;
    }

    /**
     * This method allows chaining of calls to add an attribute to this element
     * models list of attribute models.
     * @param attrMold
     * @return this element model
     */
    public ElementMold addAttrMold(AttrMold attrMold) {
        this.attrMolds.add(attrMold);
        return this;
    }

    @Override
    public ElementMold addComponentMold(ComponentMold componentMold) {
        return (ElementMold) super.addComponentMold(componentMold);

    }

    /**
     * @return the content
     */
    public String getContent() {
        return content;
    }

    /**
     * @param content the content to set
     * @return this ElementModel so that mutations can be chained together
     */
    public ElementMold setContent(String content) {
        this.content = content;
        return this;
    }

    /**
     * This ConcreteElement is a trivial implementation of a {@link PVCElement}.
     */
    private class ConcreteElement extends CompositeElement implements PVCElement {

        public ConcreteElement(Document document) {
            super(document.createElementNS(nameSpace, tagName));
        }

        @Override
        public Element gluePVC() {
            return this;
        }
    }

    /**
     * A {@link PVCComponent} based on this {@link ElementMold}.
     */
    public class Casting extends CompositeElement implements PVCElement {

        private String id;

        public Casting(Document document) {
            PVCElement pvc = new ConcreteElement(document);
            for (AttrMold attrMold : attrMolds) {
                pvc = attrMold.new ElementDecorator(pvc);
            }
            if (content != null) {
                pvc.setTextContent(content);
            } else {
                for (ComponentMold componentMold : getComponentMolds()) {
                    if (componentMold instanceof ElementMold) {
                        pvc = ((ElementMold) componentMold).new ElementDecorator(pvc);
                    } else {
                        pvc = componentMold.new ElementDecorator(pvc);
                    }
                }
            }
            this.setUnderlyingElement(pvc.gluePVC());
        }

        @Override
        public Element gluePVC() {
            return this;
        }
    }

    /**
     * Decorator Pattern for a {@link PVCElement}.  Instances of this class will
     * decorate the {@link PVCElement} passed to the constructor by appending a
     * {@link PVCElement} from this {@link ElementMold} to it.
     */
    public class ElementDecorator extends PVCElementDecorator {

        private PVCElement decoration;

        public ElementDecorator(PVCElement element) {
            super(element);
        }

        @Override
        protected void decorate() {
            this.decoration = new Casting(this.getOwnerDocument());
            this.appendChild(this.decoration);
        }

        /**
         * @return the decoration
         */
        public PVCElement getDecoration() {
            return decoration;
        }
    }

    /**
     * Decorator Pattern for a {@link PVCComponent}.  Instances of this class will
     * decorate the {@link PVCComponent} passed to the constructor by appending a
     * {@link PVCElement} from this {@link ElementMold} to it.
     */
    public class ComponentDecorator extends PVCComponentDecorator {

        private PVCElement decoration;

        public ComponentDecorator(PVCComponent component) {
            super(component);
        }

        @Override
        protected void decorate() {
            this.decoration = new Casting(this.getOwnerDocument());
            this.appendChild(this.decoration);
        }

        /**
         * @return the decoration
         */
        public PVCElement getDecoration() {
            return decoration;
        }
    }
}
